了解Java GC (译)
最近在了解Java Gc相关内容 看到一篇不错的入门文章 原文Understanding Java Garbage Collection
本文是“成为一个Java GC专家”系列的第一篇
想知道Java中GC的好处吗?
满足一个软件知识分子的好奇心是一个很好的原因 但是不仅如此 明白GC是如何工作的可以帮助您写出更好的Java程序
这里的仅仅只是我个人的主观的想法 但是我觉得 一个精通GC的人往往是一个更好的Java程序员 如果您对GC的过程感兴趣 代表着您有开发某种规模应用程序的经验 如果您仔细考虑了如何选择正确的GC算法 这意味着你完全了解你开发的应用程序的功能 当然 这个条件不能适合所有好的开发者
但是 当我说懂GC是一个好的Java开发者的必要条件时 很少有人反对
这是“成为一个Java GC专家”系列的第一篇 这里我将介绍GC的大概内容 在下一篇 我讨论来自NHN的例子中的分析GC的状态与调优
有一个术语 需要在正式学习GC之前了解 这个就是“stop-the-world” 无论选择什么GC算法都会发生stop-the-world ,Stop-the-world 意味着JVM在执行GC时停止应用 当stop-the-world发生时 所有的线程除了GC的相关线程都将停止工作 当GC完成时 中断的任务会继续工作 GC的优化其实就是减少stop-the-world的时间
分代GC收集
Java中没有明确指定删除内存的程序代码 一些人使用设置相关对象为null 或者使用System.Gc()方法来明确删除内存 设置为null倒是并无太大所谓 但是使用System.Gc()方法将会大幅影响系统的性能,是不能这样做的(非常感动的是,我并没有在NHN中发现有开发者使用这种代码)
在Java中 开发者们不能在代码中显示的删除内存,是GC发现不必要(垃圾)的对象并且移出它们在内存的空间,GC是基于以下俩个假说创建的(应该叫推测或者先决条件更准确些 而不是假说)
大多数对象很快的就会无法访问的
从老年代到年轻代的引用存在很少
这个假说被称为weak generational hypothesis(不知如何翻译) 所以为了保持这个假说的优势 在HotSpot VM中物理上的分为了俩个 年轻代与老年代
年轻代:大多数新创建的线程都在这里 由于大多数创建的对象很快就变的无法访问 许多对象都是在年轻代中创建的 然后消失 当对象从这个区域消失时 我们就可以说“minor GC”发生了
老年代:对象们无法变成无法访问并且存活的从年轻代中复制到这里 它通常比年轻代要大 而且随着大小的增加 年轻大的GC的发生也越来越不频繁 当这些对象在老年代中消失 ,我们就说它是“major GC”或者是(“Full GC”)发生了
让我们看看下面的图
图 1: GC 区域&数据流
常驻代在图表中被称为“method area” 它存储类或者内部的字符串 所以 这些对象绝对不是从老年代中存活下来到这里永久保存的 GC可能会发生在这里 这个地方发生GC也被视为Major GC
有人可能会想
如果一个老年代的对象需要引用一个年轻代的对象怎么办
在这种情况下 在老年代中有一个名为“card table”的东西的 这是一个512字节的块 如果有老年代对象引用年轻代对象 就会在这个表中记录 如果在年轻代发生GC 仅搜索card table来决定这次GC的内容 而不是去查找老年代中所有对象的引用 这个Card table是通过写屏障进行管理 这种写屏障是可以让minor GC的性能更好 虽然有一些消耗 但是总体GC的时间变少了
图 2: Card Table 结构
年轻代中的组成
要了解GC,我们了解一下年轻代的,这些对象都是第一次创建的对象 , 年轻代分为三个部分
- 一个Eden区
- 俩个Survivor 区
总共有3个区域,其中俩个是Survivor区 每个空间的执行顺序如下:
- 大多数新创建的对象都在Eden区
- 在Eden区的一次GC后 存活的对象会将移动到Survivor区
- 在Eden区Gc后,这些对象会堆积到Survivor区 并且其他幸存的对象也在这里
- 当Survivor区满了之后继续存活的对象会到另外一个Survivor区,然后,已满的Surivor的空间将会改变为没有数据的状态
5.在这些步骤中幸存的对象已经重复了很多次 会将移动到老年代中
检查这些步骤可以发现 其中一个Survivor区必须保持为空 如果数据存在于俩个Survivor空间中 或者俩个空间都没有使用 这说明系统出现了一些错误
数据通过minor GC处理堆积到老年代的过程如下图所示:
图 3: GC之前和GC之后
注意在 HotSpot VM中 有俩种技术来用于更好的内存分配 一种是“bump-the-pointer”另外一种是“TLABs(Thread-Local Allocation Buffers”
bump-the-pointer技术是跟踪分配给Eden的最后一个对象 这个对象将位于Eden的顶部 而且之后创建一个对象 它只会检查对象的大小是否适合Eden空间 如果是可以的话 它会将被放到Eden空间中 并且新对象会放到Eden中的顶部 因此 当创建新的对象时 只需要检查最后添加的对象 这样可以更快的进行对象的内存分配 但是 如果考虑在多线程的环境中就会有不同的情况 以线程安全的多线程方式保存对象到Eden空间中 将不可避免的发生锁定 并且性能将会因为锁定争用导致性能下降 在HotSpot VM中 TLABs是这种问题的解决方案 它允许不同的线程有自己共享的Eden空间中对应的一小部分 因为每个线程只能访问自己的TLAB 所以即是指针技术也允许没有锁定的内存分配
快速过了一下年轻代内存的概述 你没有必要一定记住上面俩个技术 你不会不知道他们 但是请你记住 首先创建的对象是在Eden空间中 而且 长时间存活的对象将会从Survivor区移动到老年代
老年代的GC
老年代中的数据一满的时候就会执行GC, 执行程序与GC的类型而变化,所以 如果你了解了更多不同的GC类型会更好的了解这个过程
- Serial GC
- Parallel GC
- Parallel Old GC (Parallel Compacting GC)
- Concurrent Mark & Sweep GC (or “CMS”)
- Garbage First (G1) GC
在这种中 serial GC是无法在服务器中操作使用的 当桌面电脑只有一个核心的cpu时就会使用这个GC类型 使用Serial GC会显著降低应用程序的性能
现在 让我们来看不同的GC类型
Serial GC (-XX:+UseSerialGC)
年轻代的GC使用在前一段中有相关介绍 这个GC在老年代中使用的算法是“标记-清扫-紧凑”
- 该算法第一步是标记老年代中的存活对象
- 然后 它去前面检测堆然后只留下幸存的对象(清扫)
- 在最后一步,它在前面的堆填充对象 使对象可以连续堆积在一起 并将堆分为俩个部分:一个有对象 一个没有对象(紧凑)
这个Serial GC适合于小内存和Cpu核心数少的情况下
Parallel GC (-XX:+UseParallelGC)
图 4: Serial GC 和 Parallel GC的不同之处
在图片中 你可以很清晰的看出Serial 与Parallel的区别 Serial GC只使用一个线程来处理GC ,Parrllel是使用多个线程来处理GC 因此更快 这个GC需要充足的内存与很多的CPU核心 所以又被称为“throughput GC.”
Parallel Old GC(-XX:+UseParallelOldGC)
Parallel Old GC是自Jdk5中更新出现的 相比Parallel GC 唯一的区别就是Old GC的GC算法 它有三个步骤 :标记-总结-压实 总结步骤会单独对之前通过GC操作幸存下来的对象进行分区 从而不同于其他标记-扫描-压实的扫描步骤 它要更复杂一点
CMS GC (-XX:+UseConcMarkSweepGC)
从图中可以看出 这个并发 标记-清扫 GC是比我解释的其他GC类型要复杂的多
早期的初始化标记很简单 它搜索离类加载器中最近的对象的幸存对象 所以,它的暂停时间会非常的短 在并发标记步骤中,跟踪并检查刚刚确认幸存的对象的相关引用对象 这一步与其他类型的区别在于其他的线程与此同时也会进行处理 。在重新标记步骤中, 检查在并行标记步骤过程中引用的新添加或者停止的对象 最后,在并行清扫过程中 ,垃圾回收发生。垃圾回收器是在其他线程依然工作时进行的 由于GC是因为这种方式进行的 所以GC停止的时间非常的短 这个CMS GC也被称为低延时GC,它常常被用对延时有着至关重要的需要的应用中
虽然这种GC类型有着stop-the-world时间很少的优点 但是也有以下的缺点
- 它比其他GC类型使用更多的CPU和内存
- 默认情况下不提供压缩操作
你需要仔细检查来使用这种类型,另外 如果因为内存碎片太多需要执行压缩 ,那么stop-the-world的时间比其他任何GC类型都要长 你需要检查压缩任务的时间与发生频率
G1 GC
如果你想要了解G1 GC,请忘记所有有关年轻代与老年代的内容
如图所示, 将一个对象分配到一个网格中 然后执行一个GC,然后,当一个区域已满,这些对象分配到另外一个区域,然后执行GC
在这个GC类型中,无法找到数据从年轻代的三个空间移动到老年代的步骤( The steps where the data moves from the three spaces of the young generation to the old generation cannot be found in this GC type. ??)。这种类型是为了取代CMS GC,长期以来CMS GC引来了很多问题和投诉
G1 GC的最大优点是性能,它比我们讨论过的所有GC类型速度都要快,但是它在JDK6中,它被称为早期访问,只是用来测试,正式出现则是在JDK7中。在我个人看来,我们需要一个很长的测试(至少一年),NHN才能在实际服务中使用JDK7, 所以说你需要等一等,另外,在JDK6中我听说过几次JVM崩溃的情况 所以说请等待更加稳定的时候再用
在下一个问题中 我们将谈论GC调优,但是我想在此之前问一些事情,如果在应用中创建的所有对象大小和类型相同,我们公司使用的所有WAS选项都可以使用,但是因为WAS创建的对象大小和寿命因服务而异,随之设备种类也随之变化,换种说法,在某些服务中使用GC选项“A”,并不代表同样的选项对其他服务也会有一样好的效果,所以有必要通过不断的调整监控,为每个设备和每个选项都提供WAS实例,找到每个WAS线程的最佳值。这不是我的个人意见,这是来自JavaOne 2010 Oracle VM组工程师们的讨论
在本文的问题上,我们只大概的看了一下Java的GC,在下一个问题中,我将介绍如何监控Java GC状态并且如何调整GC